package client;
import java.io.*;
import javax.swing.*;

import msgs.ProfileMsg;

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.event.*;

/**
 * Provides many of the functions needed for AIM connectivity, including buddy list updating
 * Team 9: Andrew Hayworth, Brian Parrella, Ryan Kortmann, Nina Papa
 * @author Andrew Hayworth
 *
 */
public class AIM extends Thread implements Chatable {
	private JPopupMenu buddyListPopup;
	private JMenuItem popupIM, popupRemoveBuddy, popupBlockBuddy, popupUnblockBuddy;
	private String pwd;
	
	/**
	 * Our username, accessed from child chat windows.
	 */
	protected static String uname;
	
	/**
	 * This is an object of the JavaTOC2 class -- represents our connection to AIM
	 */
	protected static JavaTOC2 jtoc2;
	
	private JFrame jf;
	private JPanel jp;
	private JComboBox jcb;
	private JList jl;
	private Vector<String> bList;
	private JScrollPane jsp;
	
	/**
	 * Running list of all chat windows open. Actually stores the chatwindow objects in the table.
	 * Keyed on username of who we're talking to
	 */
	protected static Hashtable<String, ChatWindow> chats;
	
	private JMenuBar jmb;
	private JMenu file, people, options;
	private JCheckBoxMenuItem soundSel;
	private JMenuItem logout, exit, newIM, addBuddy, removeBuddy, getInfo, block, unblock;
	private JTextField jtfNewIm;
	private JDialog jd;
	
	/**
	 * Hackish method of keeping track of a buddy's status.
	 */
	protected static Hashtable<String, Integer> status;
	
	private AOLBLRenderer aolblr;
	
	/**
	 * This is a list of groups set up on the AIM buddylist, and who's in them.
	 * This is needed because if we try to remove a buddy, AIM requires that you specify their group
	 */
	protected static Hashtable<String, ArrayList<String>> groupsBuddies;
	
	/**
	 * This is just an arraylist of everything that AOL sends us when we log in
	 */
	protected static ArrayList<String> config2;
	
	private JTextArea awayMessageEditor;
	
	/**
	 * We have to manually keep track of who we should be ignoring, as AOL doesn't do it at teh
	 * server level.
	 */
	protected static ArrayList<String> ignoring;
	
	// My gui setup code is terrible. I apologize in advance.
	public AIM(String uname, String pwd) {
		// setting up our aim global hashtables...
		ignoring = new ArrayList<String>();
		config2 = new ArrayList<String>();
		groupsBuddies = new Hashtable<String, ArrayList<String>>();
		status = new Hashtable<String, Integer>();
		chats = new Hashtable<String, ChatWindow>();
		bList = new Vector<String>();
		
		this.uname = uname;
		this.pwd = pwd;
		jf = new JFrame();
		jf.setSize(210, 600);
		jf.setLocationRelativeTo(null);
		
		jmb = new JMenuBar();
		file = new JMenu("File");
		file.setMnemonic('F');
		people = new JMenu("People");
		people.setMnemonic('P');
		
		logout = new JMenuItem("Logout");
		logout.setMnemonic('L');
		exit = new JMenuItem("Exit");
		exit.setMnemonic('E');
		newIM = new JMenuItem("New IM");
		newIM.setMnemonic('N');
		addBuddy = new JMenuItem("Add Buddy");
		addBuddy.setMnemonic('A');
		removeBuddy = new JMenuItem("Remove Buddy");
		removeBuddy.setMnemonic('R');
		getInfo = new JMenuItem("Get Info");
		getInfo.setMnemonic('G');
		block = new JMenuItem("Block User");
		block.setMnemonic('B');
		unblock = new JMenuItem("Unblock User");
		unblock.setMnemonic('U');
		
		options = new JMenu("Options");
		options.setMnemonic('O');
		soundSel = new JCheckBoxMenuItem("Sound");
		soundSel.setMnemonic('S');
		soundSel.setSelected(true);
		options.add(soundSel);
		
		jmb.add(file);
		jmb.add(people);
		jmb.add(options);
		
		file.add(logout);
		file.add(exit);
		
		people.add(newIM);
		people.add(addBuddy);
		people.add(removeBuddy);
		//people.add(getInfo);
		people.add(block);
		people.add(unblock);
		
		jf.setJMenuBar(jmb);
		
		soundSel.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent ce) {
				if (soundSel.isSelected()) {
					TTClient.setSoundOk(true);
					//soundSel.setSelected(false);
				}
				else {
					TTClient.setSoundOk(false);
					//soundSel.setSelected(true);
				}
			}
		});
		
		// popup menu for the aim buddylist.
		buddyListPopup = new JPopupMenu();
		popupIM = new JMenuItem("Send Message");
		popupRemoveBuddy = new JMenuItem("Remove User");
		popupBlockBuddy = new JMenuItem("Block User");
		buddyListPopup.add(popupIM);
		buddyListPopup.add(popupRemoveBuddy);
		buddyListPopup.add(popupBlockBuddy);
		
		// status message setting
		jp = new JPanel();
		String[] statuschoices = { "Available", "Away" };
		jcb = new JComboBox(statuschoices);
		jcb.setEditable(false);
		
		jp.add(jcb);
		jf.add(jcb, BorderLayout.NORTH);
		
		jf.setTitle("TigerTalk - AIM - " + uname);
		jf.setVisible(true);
		
		jl = new JList( bList );
		aolblr = new AOLBLRenderer();	// we use a custom jlist cell renderer to get the cells to change color depending on status
	    jl.setCellRenderer(aolblr);
		jsp = new JScrollPane(jl);
		
		// TODO fix me! We need to make selection status more obvious, this color
		// scheme isn't really working out too well
		jl.setSelectionBackground(Color.WHITE);
		jl.setSelectionForeground(Color.BLACK);
		
	
		jf.add(jsp, BorderLayout.CENTER);
		
		jf.addWindowListener( new WindowAdapter() {
			public void windowClosing(WindowEvent we) {
				jtoc2.logout();
				System.exit(0);
			}
		});
		
		// yay, swing makes things easy! "need to implement mouselisteners" yeah right!
		jl.setComponentPopupMenu(buddyListPopup);
		popupIM.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				String s = jl.getSelectedValue().toString();
				if (s != null ) {	//null test, hate those nasty exceptions...
					if (chats.containsKey(s)) {
						chats.get(s).alert();
					}
					else {
						chats.put(s, new ChatWindow(s));
					}
				}
			}
		});
		popupRemoveBuddy.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				removeBuddy(jl.getSelectedValue().toString());
			}
		});
		popupBlockBuddy.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				blockBuddy(jl.getSelectedValue().toString());
			}
		});
		
		// mouselistener for when they doubleclick on a buddy in the list.
		jl.addMouseListener(new MouseAdapter() {
			public void mouseClicked(MouseEvent me) {
				if (me.getClickCount() == 2) {
					int index = jl.locationToIndex(me.getPoint());
					ListModel lm = jl.getModel();
					String s = (String) lm.getElementAt(index);
					if (chats.containsKey(s)) {
						chats.get(s).alert();
					}
					else {
						chats.put(s, new ChatWindow(s));
					}
					
				}
			}
		});
		
		exit.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				jtoc2.logout();
				jtoc2.killJTOC2();
				System.exit(0);
			}
		});
		
		logout.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				jtoc2.logout();
				jtoc2.killJTOC2();
				jf.dispose();
				new TTClient();
			}
		});
		
		//this lets us send a new im to a random person, which may or may not be in our buddylist
		newIM.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				jd = new JDialog();
				JPanel jp = new JPanel();
				jtfNewIm = new JTextField(10);
				JLabel jl = new JLabel("Username: ");
				JButton send = new JButton("Send");
				send.setMnemonic('S');
				jp.add(jl);
				jp.add(jtfNewIm);
				jp.add(send);
				jd.add(jp);
				jd.pack();
				jd.setLocationRelativeTo(null);
				jd.setTitle("New IM");
				jd.setVisible(true);
				send.addActionListener(new ActionListener() {
					public void actionPerformed(ActionEvent ae) {
						if (!(jtfNewIm.getText().equals(""))) {
							if (chats.containsKey(jtfNewIm.getText())) {
								chats.get(jtfNewIm.getText()).alert();
							}
							else {
								chats.put(jtfNewIm.getText(), new ChatWindow(jtfNewIm.getText()));
							}
						}
						jd.dispose();
					}
				});
			}
		});
		
		// this isn't working yet. It may never. AOL does weird things when we request a profile....
		getInfo.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				jd = new JDialog();
				JPanel jp = new JPanel();
				jtfNewIm = new JTextField(10);
				JLabel jl = new JLabel("Username: ");
				JButton send = new JButton("Get Info");
				send.setMnemonic('G');
				jp.add(jl);
				jp.add(jtfNewIm);
				jp.add(send);
				jd.add(jp);
				jd.pack();
				jd.setLocationRelativeTo(null);
				jd.setTitle("Get Info");
				jd.setVisible(true);
				send.addActionListener(new ActionListener() {
					public void actionPerformed(ActionEvent ae) {
						if (!(jtfNewIm.getText().equals(""))) {
							try {
								AIM.jtoc2.sendFlap(JavaTOC2.DATA, "toc_get_info \"" + jtoc2.normalize(jtfNewIm.getText()) + "\"\0");
							}
							catch (Exception e) {}
						}
						jd.dispose();
					}
				});
			}
		});
		
		// adds a new buddy to the buddy list
		addBuddy.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				jd = new JDialog();
				JPanel jp = new JPanel();
				jtfNewIm = new JTextField(10);
				JLabel jl = new JLabel("Username: ");
				JButton send = new JButton("Add");
				send.setMnemonic('A');
				jp.add(jl);
				jp.add(jtfNewIm);
				jp.add(send);
				jd.add(jp);
				jd.pack();
				jd.setLocationRelativeTo(null);
				jd.setTitle("Add Buddy");
				jd.setVisible(true);
				send.addActionListener(new ActionListener() {
					public void actionPerformed(ActionEvent ae) {
						if (!(jtfNewIm.getText().equals("")) && !(jtfNewIm.getText().equals(AIM.uname))) {
							try {
								// toc2 requires a buddy to be in a group. in the interest of simplicity, 
								// we just add everyone to a "Friends" group. a lot of other clients do similar things.
								AIM.jtoc2.sendFlap(JavaTOC2.DATA, "toc2_new_buddies {g:Friends\nb:" + jtfNewIm.getText()+"\n}");
								// we now need to ensure that OUR list of groups has friends, and has them on that list...
								if (!(AIM.groupsBuddies.containsKey("Friends"))) {
									ArrayList<String> al = new ArrayList<String>();
									al.add(jtfNewIm.getText());
									AIM.groupsBuddies.put("Friends", al);
								}
								else AIM.groupsBuddies.get("Friends").add(jtfNewIm.getText());
							}
							catch (Exception e) {}
						}
						jd.dispose();
					}
				});
			}
		}); 
		
		// does just what it sounds like.
		removeBuddy.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				jd = new JDialog();
				JPanel jp = new JPanel();
				jtfNewIm = new JTextField(10);
				JLabel jl = new JLabel("Username: ");
				JButton send = new JButton("Remove");
				send.setMnemonic('R');
				jp.add(jl);
				jp.add(jtfNewIm);
				jp.add(send);
				jd.add(jp);
				jd.pack();
				jd.setLocationRelativeTo(null);
				jd.setTitle("Remove Buddy");
				jd.setVisible(true);
				send.addActionListener(new ActionListener() {
					public void actionPerformed(ActionEvent ae) {
						if (!(jtfNewIm.getText().equals(""))) {
							removeBuddy(jtfNewIm.getText());
						}
						jd.dispose();
					}
				});
			}
		});
		
		// blocks the buddy from contacting us.
		block.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				jd = new JDialog();
				JPanel jp = new JPanel();
				jtfNewIm = new JTextField(10);
				JLabel jl = new JLabel("Username: ");
				JButton send = new JButton("Block");
				send.setMnemonic('B');
				jp.add(jl);
				jp.add(jtfNewIm);
				jp.add(send);
				jd.add(jp);
				jd.pack();
				jd.setLocationRelativeTo(null);
				jd.setTitle("Block User");
				jd.setVisible(true);
				send.addActionListener(new ActionListener() {
					public void actionPerformed(ActionEvent ae) {
						if (!(jtfNewIm.getText().equals(""))) {
							blockBuddy(jtfNewIm.getText());
						}
						jd.dispose();
					}
				});
			}
		});
		
		// converse of the previous code block
		unblock.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				jd = new JDialog();
				JPanel jp = new JPanel();
				jtfNewIm = new JTextField(10);
				JLabel jl = new JLabel("Username: ");
				JButton send = new JButton("Unblock");
				send.setMnemonic('U');
				jp.add(jl);
				jp.add(jtfNewIm);
				jp.add(send);
				jd.add(jp);
				jd.pack();
				jd.setLocationRelativeTo(null);
				jd.setTitle("Unblock User");
				jd.setVisible(true);
				send.addActionListener(new ActionListener() {
					public void actionPerformed(ActionEvent ae) {
						if (!(jtfNewIm.getText().equals(""))) {
							try {
								// toc 2 has 2 different main modes of blocking people. 
								// I dont have the patience nor the time to fully support both, so we just tell toc2 
								// to unblock both ways. toc2 just silently ignores incorrect commands, so we just send
								// both.
								jtoc2.sendFlap(JavaTOC2.DATA, "toc2_add_permit " + jtoc2.normalize(jtfNewIm.getText()));
								jtoc2.sendFlap(JavaTOC2.DATA, "toc2_remove_deny " + jtoc2.normalize(jtfNewIm.getText()));
								AIM.ignoring.remove(jtfNewIm.getText());
							}
							catch (Exception e) {}
						}
						jd.dispose();
					}
				});
			}
		});
		
		// the jcb is just a jcombobox where you can set your away status
		jcb.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent ae) {
				try {
					if (jcb.getSelectedItem().toString().equals("Available")) {
						jtoc2.sendFlap(JavaTOC2.DATA, "toc_set_away");	// no arguments on this string causes toc2 to interpret it as "available"
					}
					else {
						jd = new JDialog();
						JPanel jp2 = new JPanel();
						awayMessageEditor = new JTextArea(10,10);
						awayMessageEditor.setText("Not here right now ...");
						JButton send = new JButton("Set");
						JButton cancel = new JButton("Cancel");
						send.setMnemonic('S');
						cancel.setMnemonic('C');
						jp2.add(send);
						jp2.add(cancel);
						jd.add(awayMessageEditor, BorderLayout.CENTER);
						jd.add(jp2, BorderLayout.SOUTH);
						jd.pack();
						jd.setLocationRelativeTo(null);
						jd.setTitle("Set Away Message");
						jd.setVisible(true);
						
						send.addActionListener(new ActionListener() {
							public void actionPerformed(ActionEvent ae) {
								try {
									// we *must* have a status message.... toc2 requires a status message, or it means that we're actually available
									// which is horribly contrary to what we want to do here.
									// so we set a default if the user tries to outsmart us.
									// bad, bad user...
									if (awayMessageEditor.getText().equals("")) {
										awayMessageEditor.setText("Away");
									}
									// need to enclose this in quotes, and terminate the string with \0
									// which is entirely unobvious from any and all documentation.
									// yay for learning from other open source projects!
									jtoc2.sendFlap(JavaTOC2.DATA, "toc_set_away \"" + jtoc2.encode(awayMessageEditor.getText()) +"\"\0");
									jd.dispose();
								}
								catch (Exception e) {}
							}
						});
						
						cancel.addActionListener(new ActionListener() {
							public void actionPerformed(ActionEvent ae) {
								jd.dispose();
							}
						});
					}
				}
				catch(IOException e) {}
			}
		});
	}
	
	public void run() {
		// need to have a reference to a JavaTOC2 object, which manages our connections.
		jtoc2 = new JavaTOC2(this);
		try {
			jtoc2.login(uname, pwd);
			jtoc2.processTOCEvents();
		}
		catch (IOException e) { e.printStackTrace(); }
	}
	
	/**
	 * this is required by the Chatable interface
	 * handles unknown commands
	 * @param str The string from AIM/TOC2
	 */
	public void unknown(String str) {
		System.out.println(str);
	}
	/**
	 * handles error message from aol/toc2
	 * @param str Error string from toc2
	 * @param var Optional variable to insert into the error string. (Not used in this implementation)
	 */
	public void error(String str, String var) { 
		int errorCode;
		try {
			errorCode = Integer.parseInt(var);
		}
		catch (NumberFormatException e) {
			System.out.println("Unknown error!");
			errorCode = 0;
		}
		
		switch (errorCode) {
		case 983:
			JOptionPane.showMessageDialog(null, "You have been connecting and disconnecting too quickly! Wait 10 minutes and try again.", "Login error!", JOptionPane.ERROR_MESSAGE);
			jf.dispose();
			jtoc2.killJTOC2();
			new TTClient();
			break;
		case 989:
			JOptionPane.showMessageDialog(null, "Unknown Signon Error", "Login Error", JOptionPane.ERROR_MESSAGE);
			jf.dispose();
			jtoc2.killJTOC2();
			new TTClient();
			break;
		case 980:
			JOptionPane.showMessageDialog(null, "Incorrect Username/Password", "Login Error", JOptionPane.ERROR_MESSAGE);
			jf.dispose();
			jtoc2.killJTOC2();
			new TTClient();
			break;
		default:
		case 0:
			break;
		}
		// keeping this for debugging reasons... nice to know when we're sending too many typing notifications.
		System.out.println("Error: " + str + " :" + var); 
	}

	/**
	 * Required by the Chatable interface. What to do when we get an instant message in
	 * @param from Username who's talking to us
	 * @param message what they said
	 */
	public void im(String from, String message) { 
		// gtfo if we're ignoring them.
		if (AIM.ignoring.contains(from) ) return;
		
		// if we have a window open already, just update that.
		if (chats.containsKey(from)) {
			chats.get(from).updateChat(message);
		}
		// otherwise make a new one.
		else {
			chats.put(from, new ChatWindow(from));
			chats.get(from).updateChat(message);
		}
	}
	
	/**
	 * Repaints the aim buddylist with a new user. Does NOT update the entire buddylist
	 * @param str the string straight from TOC2 describing the buddy update.
	 */
	public void updateBuddyList(String str ) {
		// toc2 splits fields on ':'
		String[] parts = str.split(":");
		int st;	// this is a temporary variable for the status we want to assign them
		
		try {
			// idle
			if (!(parts[5].equals("0"))) st = 3;
			
			// available
			if (parts[6].equals(" U") || parts[6].equals(" AU") || parts[6].equals(" A") || parts[6].equals(" O")) st = 0;
			
			// unavailable
			else if (parts[6].equals(" OU") || parts[6].equals(" UU")  ) st = 1;
			
			// invisible/mobile/etc
			// C = mobile, need to add that in....
			else st = 2;
			//if (!(parts[5].equals("0"))) st = 3;
		}
		catch (ArrayIndexOutOfBoundsException e) {
			// sometimes TOC sends us a packet that is too short, not all fields are there. i don't want to die on that error!
			st = 2; // just set that buddy as invisible.
		}
		
		// T = actually online
		if (parts[2].toUpperCase().equals("T")) {
			// add them to our internal buddylist if they're not there
			if (!(bList.contains(parts[1]))) bList.add(parts[1]);
			
			// add them to our internal status table if theyre not there
			if (!(status.containsKey(parts[1]))) {
				status.put(parts[1], st);
			}
			// otherwise update them in that table
			else {
				status.remove(parts[1]);
				status.put(parts[1], st);
			}
			TTClient.playLogin();
		}
		
		// offline!
		if (parts[2].toUpperCase().equals("F")) {
			if (bList.contains(parts[1])) bList.remove(parts[1]);
			TTClient.playLogout();
		}
		
		Collections.sort(bList);
		
		jl.setListData(bList);
		jl.repaint();
		// UGLY HACK!
		// have to do this on some platforms or we dont actually get an updated jlist
		// write once, run everywhere my ass.
		// write once, debug everywhere sounds better.
		jf.setVisible(true);
	}
	
	// removes the buddy from the server-side list
	private void removeBuddy(String b) {
		try {
			String g = null;
			for (String s: AIM.groupsBuddies.keySet()) {
				// see if we know what group they're in
				if (AIM.groupsBuddies.get(s).contains(b)) g = s;
				System.out.println(b + " is in group " + g);
			}
		
			if (g != null) jtoc2.sendFlap(JavaTOC2.DATA, "toc2_remove_buddy " + jtoc2.normalize(b) + " \"" + g +"\"") ;
			// well, we don't know what group they're in. leave it off and hope for the best..
			else jtoc2.sendFlap(JavaTOC2.DATA, "toc2_remove_buddy " + jtoc2.normalize(b));
			// we have to send a dummy string to our updateBuddylist method, as AIM doesnt really return anything helpful
			// when you remove a buddy
			updateBuddyList("UPDATE_BUDDY2:" + b +":F:0:1242453395:0: O:0");
		}
		catch (Exception e) {}
	}
	
	// blocks a buddy
	private void blockBuddy(String b) {
		try {
			//block them both ways. doesnt matter if we send the incorrect method, as toc2 will just ignore it.
			jtoc2.sendFlap(JavaTOC2.DATA, "toc2_remove_permit " + jtoc2.normalize(b));
			jtoc2.sendFlap(JavaTOC2.DATA, "toc2_add_deny " + jtoc2.normalize(b));
			// add them to our internal list, which we have to use because blocking only happens at the client level.
			AIM.ignoring.add(b);
		}
		catch (Exception e) {}
	}
}

/**
 * Private cell renderer for our buddylist
 * @author ash8774
 */
class AOLBLRenderer extends DefaultListCellRenderer {
	// local copy of the status hashtable;
	// status table -- 0 - online, 1 - away, 2 - invisible, 3 - idle
	private Hashtable <String, Integer> status;
	
	/**
	 * Overriden from DefaultListCellRenderer
	 */
	public Component getListCellRendererComponent(JList list, 
            Object value,
            int index, 
            boolean isSelected,
            boolean cellHasFocus) {

		super.getListCellRendererComponent(list, 
                value, 
                index, 
                isSelected, 
                cellHasFocus);
		
		status = AIM.status;
		if (status.get(value.toString()) == 0) {
			setBackground(Color.GREEN);
		}
		if (status.get(value.toString()) == 1) {
			setBackground(Color.RED);
		}
		if (status.get(value.toString()) == 2) {
			setBackground(Color.lightGray);
		}
		if (status.get(value.toString()) == 3) {
			setBackground(Color.YELLOW);
		}
		
		return this;
	}
}
